package org.hilo.boot.app.config;

import java.net.UnknownHostException;

import org.apache.commons.pool2.impl.GenericObjectPool;
import org.hilo.boot.core.redis.JedisConnectionConfiguration;
import org.hilo.boot.core.redis.LettuceConnectionConfiguration;
import org.hilo.boot.core.redis.RedisTemplateConfiguration;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.data.redis.JedisClientConfigurationBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.LettuceClientConfigurationBuilderCustomizer;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.data.redis.connection.RedisClusterConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisSentinelConfiguration;
import org.springframework.data.redis.connection.jedis.JedisConnection;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration.LettuceClientConfigurationBuilder;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import io.lettuce.core.RedisClient;
import io.lettuce.core.cluster.ClusterClientOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions;
import io.lettuce.core.cluster.ClusterTopologyRefreshOptions.RefreshTrigger;
import io.lettuce.core.resource.ClientResources;
import io.lettuce.core.resource.DefaultClientResources;
import redis.clients.jedis.Jedis;

/**
 * <p>默认Redis源配置（覆盖spring默认的spring.boot.autoconfigure.data.redis.RedisAutoConfiguration）</p>
 * 
 * <p>如需其他Redis源{XXX}，新建一个 RedisConfig{XXX} 类继承 AbstractRedisConfig，
 *    参见Demo：{@link RedisConfigFoo2}</p>
 * 
 * @author zollty
 * @since 2018年6月26日
 */
@Configuration
@ConditionalOnClass(RedisOperations.class)
@ConditionalOnProperty(name = "hilo.enable-default-redis", havingValue = "true", matchIfMissing=true)
public class RedisConfig {
    public static final String PREFIX = "hilo.multi.redis.";
    public static final String NAMES = "hilo.multi.redis.names";

    @Primary
    @Bean
    @ConfigurationProperties(prefix = "spring.redis")
    public RedisProperties redisProperties() {
        return new RedisProperties();
    }
    
    @Configuration
    @ConditionalOnClass(RedisClient.class)
    public static class DefaultLettuceConnectionFactory {
        
        @Bean(destroyMethod = "shutdown")
        @ConditionalOnMissingBean(ClientResources.class)
        public DefaultClientResources lettuceClientResources() {
            return DefaultClientResources.create();
        }
        
        /**
         * 解决问题：
         *  节点异常，服务端的Redis集群拓扑被刷新了，但是本地没有获取到新的拓扑。
         *  现象：redis cluster集群有一台机崩了之后，后台服务的redis会一直报错，无法连接到redis集群（RedisCommandTimeoutException）。
         *  
         *  lettuce客户端支持的 自适应拓扑刷新（Adaptive updates）与定时拓扑刷新（Periodic updates） 是默认关闭的。
         * 
         * see https://github.com/lettuce-io/lettuce-core/wiki/Redis-Cluster
         * see https://lettuce.io/core/snapshot/reference/#client-options
         */
        @Bean
        @ConditionalOnProperty(name = "hilo.redis.lettuce.enable-topology-refresh", havingValue = "true", matchIfMissing=true)
        public LettuceClientConfigurationBuilderCustomizer lettuceClientCustomizer() {
            return new LettuceClientConfigurationBuilderCustomizer() {
                @Override
                public void customize(LettuceClientConfigurationBuilder clientConfigurationBuilder) {
                    
                    ClusterTopologyRefreshOptions topologyRefreshOptions = ClusterTopologyRefreshOptions.builder()
                            // 周期性刷新
//                            .enablePeriodicRefresh() 
                            // 事件触发性刷新（常见三种情况：收到-ASK、-MOVED指令 或者 物理重连接，其中）
                            // By default, commands follow -ASK and -MOVED redirects up to 5 times until the command execution is considered to be failed
                            .enableAdaptiveRefreshTrigger(RefreshTrigger.MOVED_REDIRECT, RefreshTrigger.PERSISTENT_RECONNECTS) 
                            .build();
                    
                    clientConfigurationBuilder.clientOptions(
                            ClusterClientOptions.builder().topologyRefreshOptions(topologyRefreshOptions).build());
                }
            };
        }
        
        @Primary
        @Bean
        @ConditionalOnMissingBean(RedisConnectionFactory.class)
        public LettuceConnectionFactory redisConnectionFactory(@Qualifier("redisProperties") RedisProperties properties,
                ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
                ObjectProvider<RedisClusterConfiguration> clusterConfiguration,
                ClientResources clientResources, 
                ObjectProvider<LettuceClientConfigurationBuilderCustomizer> builderCustomizers) throws UnknownHostException {
            return new LettuceConnectionConfiguration(properties, sentinelConfiguration, 
                    clusterConfiguration, builderCustomizers).redisConnectionFactory(clientResources);
        }
    }
    
    @Configuration
    @ConditionalOnClass({ GenericObjectPool.class, JedisConnection.class, Jedis.class })
    public static class DefaultJedisConnectionFactory {
        @Primary
        @Bean
        @ConditionalOnMissingBean(RedisConnectionFactory.class)
        public JedisConnectionFactory redisConnectionFactory(@Qualifier("redisProperties") RedisProperties properties,
                ObjectProvider<RedisSentinelConfiguration> sentinelConfiguration,
                ObjectProvider<RedisClusterConfiguration> clusterConfiguration,
                ObjectProvider<JedisClientConfigurationBuilderCustomizer> builderCustomizers) throws UnknownHostException {
            return new JedisConnectionConfiguration(properties, sentinelConfiguration, 
                    clusterConfiguration, builderCustomizers).redisConnectionFactory();
        }
    }

    /**
     * RedisTemplate configuration.
     */
    @Configuration
    @ConditionalOnBean(name = "redisConnectionFactory")
    public static class DefaultTemplateConfig extends RedisTemplateConfiguration {

        public DefaultTemplateConfig(
                @Qualifier("redisConnectionFactory") 
                RedisConnectionFactory connectionFactory) {
            super(connectionFactory);
        }
        
        @Primary
        @Bean
        @ConditionalOnMissingBean(name = "redisTemplate")
        public RedisTemplate<Object, Object> redisTemplate()
                        throws UnknownHostException {
            RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
            template.setConnectionFactory(connectionFactory);
            return template;
        }

        @Bean
        @ConditionalOnMissingBean(StringRedisTemplate.class)
        public StringRedisTemplate stringRedisTemplate()
                        throws UnknownHostException {
            StringRedisTemplate template = new StringRedisTemplate();
            template.setConnectionFactory(connectionFactory);
            return template;
        }
        
        /** 对于(Object implements Serializable)，使用JDK对象序列化 */
        @Bean
        RedisTemplate<String, java.io.Serializable> serialRedisTemplate() {
            return initJdkSerialTemplate(java.io.Serializable.class);
        }

        /** 对于Number类型，使用ToString序列化 */
        @Bean
        RedisTemplate<String, Number> numRedisTemplate() {
            return initGenericToStrTemplate(Number.class);
        }

        /** 定义一个ValueOperations，方便日常使用 */
        @Bean
        ValueOperations<String, String> strOperations(StringRedisTemplate redisTemplate) {
            return redisTemplate.opsForValue();
        }

    }

}
